feat: Harness Phase 01 — search, git tools, and bash TUI approval#13
Conversation
…hase 01) Deliver HARNESS-01/02/03 with ripgrep-backed grep, gitStatus/gitDiff read-only tools, and a blocklist-driven bash approval dialog in Build mode aligned with Claude Code permission separation (TUI-only gate, no chat re-confirm after reject). Co-authored-by: Cursor <cursoragent@cursor.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
More reviews will be available in 43 minutes and 21 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
📝 WalkthroughWalkthroughThis PR adds bash approval handling in Build mode, new dialog and chat wiring for approve/reject/session actions, ripgrep- and git-backed local tools, updated system prompts, and supporting docs/tests. ChangesBuild-mode approvals and local tools
Sequence Diagram(s)sequenceDiagram
participant useChat as useChat.onToolCall
participant requestBashApproval as requestBashApproval
participant DialogProvider
participant BashApprovalDialog
participant executeLocalTool
participant chatAddToolOutput as chat.addToolOutput
participant User
useChat->>requestBashApproval: ask for bash approval
requestBashApproval->>DialogProvider: open BashApprovalDialog
DialogProvider->>BashApprovalDialog: render dialog
User->>BashApprovalDialog: choose approve / reject / allow-session
BashApprovalDialog->>requestBashApproval: resolve verdict
requestBashApproval-->>useChat: verdict
alt reject
useChat->>chatAddToolOutput: output-error
else approve once or allow-session
useChat->>executeLocalTool: run bash
executeLocalTool-->>useChat: tool output
useChat->>chatAddToolOutput: output or output-error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (2)
packages/shared/src/schemas.test.ts (1)
31-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the exact PLAN/BUILD tool contract.
The PLAN test only checks inclusion, so it would still pass if
bashor write tools accidentally leaked into PLAN mode.✅ Proposed test tightening
test("PLAN includes gitStatus and gitDiff", () => { const tools = getToolContracts(Mode.PLAN); - expect(Object.keys(tools)).toContain("gitStatus"); - expect(Object.keys(tools)).toContain("gitDiff"); + expect(Object.keys(tools).sort()).toEqual( + ["readFile", "listDirectory", "glob", "grep", "gitStatus", "gitDiff"].sort(), + ); }); test("BUILD includes git tools plus write/bash", () => { const tools = getToolContracts(Mode.BUILD); - expect(Object.keys(tools)).toContain("gitStatus"); - expect(Object.keys(tools)).toContain("gitDiff"); - expect(Object.keys(tools)).toContain("bash"); + expect(Object.keys(tools).sort()).toEqual( + [ + "readFile", + "listDirectory", + "glob", + "grep", + "gitStatus", + "gitDiff", + "writeFile", + "editFile", + "bash", + ].sort(), + ); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/schemas.test.ts` around lines 31 - 43, The getToolContracts tests are too permissive for Mode.PLAN because they only assert inclusion and would miss accidental tool leaks. Tighten the assertions in schemas.test.ts around getToolContracts so the PLAN case verifies the exact returned contract only contains the expected git tools and excludes write/bash tools, while the BUILD case still confirms the full expected set; use getToolContracts and Mode as the anchor points.packages/cli/src/providers/dialog/index.tsx (1)
33-38: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winMove
onCloseout of the React state updater.
setCurrentDialog((prev) => { ... })is doing imperative work inside a state updater. React can replay updater functions, so this callback contract is not guaranteed to fire exactly once. Read the current dialog first, invokeonClose, then clear state.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli/src/providers/dialog/index.tsx` around lines 33 - 38, The close callback in the dialog provider is performing the onClose side effect inside the setCurrentDialog updater, which can be replayed by React. Update the close useCallback in the dialog context so it first reads the current dialog from state, calls currentDialog.onClose if present, and then clears the dialog state separately; keep the logic out of the state updater and preserve the existing close behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/agent-permissions.md`:
- Line 7: The docs reference the wrong enforcement layer for blocked bash
commands. Update the text in agent-permissions.md to point to bash-approval.ts,
since requiresApproval(...) is only called from use-chat.ts and the actual
blocklist matching lives in packages/cli/src/lib/bash-approval.ts; use that
symbol to guide readers to the correct implementation location.
In `@packages/cli/src/hooks/use-chat.ts`:
- Around line 135-168: Move the bash input parsing for `toolCall.toolName ===
"bash"` inside the same `try/catch` that wraps `executeLocalTool` in
`use-chat.ts`, so malformed input from `toolInputSchemas.bash.parse` is also
converted into a terminal `output-error`. Keep the approval logic
(`requiresApproval`, `requestBashApproval`, `rememberSessionAllow`) intact, but
ensure any parse failure or other bash setup error is caught and passed to
`chat.addToolOutput` with `state: "output-error"` and `errorText` instead of
escaping before the catch.
- Line 126: The tool-call mode lookup in use-chat should not default from the
last message blindly, because the latest assistant/tool-continuation message may
have no metadata and can incorrectly fall back to BUILD. Update the mode
selection logic around the message metadata lookup so it scans backward for the
most recent metadata-bearing message and uses that message’s mode instead of
chat.messages.at(-1), ensuring executeLocalTool still respects PLAN-mode
read-only behavior.
In `@packages/cli/src/lib/bash-approval-ui.ts`:
- Around line 31-50: The requestBashApproval flow can hang because
DialogProvider.open() replaces the current dialog without settling the existing
one, so the promise in bash-approval-ui.ts never resolves when a reentrant
dialog opens. Update DialogProvider.open() to close or otherwise settle any
existing dialog before assigning the new currentDialog, and make sure the
existing dialog’s onClose path is triggered so requestBashApproval is not left
pending.
In `@packages/cli/src/lib/bash-approval.ts`:
- Around line 20-21: Expand the approval blocklist in bash-approval.ts so
destructive commands are caught in more common forms: update the rm pattern used
in the approval checks to match uppercase recursive flags like rm -R and mixed
forms like rm -Rf, and update the git push pattern to also catch refspec force
syntax such as git push origin +main in the same validation logic. Keep the
changes localized to the existing regexes near the rm and git push rules so the
Build-mode dialog still blocks these variants.
In `@packages/cli/src/lib/local-tools.ts`:
- Around line 141-151: The gitStatus result in local-tools is undercounting
dirty worktree entries because it only adds modified and deleted files to
unstaged, so other non-clean states can be missed. Update the git status mapping
to include all dirty worktree buckets from the git status object when computing
unstaged, and adjust the summary formatting in gitStatus so it reflects the full
dirty state rather than only staged/modified/untracked counts.
In `@packages/cli/src/lib/ripgrep.ts`:
- Around line 44-46: The argv built in ripgrep’s argument assembly is not stable
enough for the output parser used by local-tools. In the ripgrep helper that
constructs args for `rg`, make sure the search always produces
`file:line:content` and cannot treat the search term as an option: add the
appropriate flag to always include filenames and insert the end-of-options
separator before `pattern` and `resolved`. This should be fixed in the
argument-building logic in the ripgrep utility so
`packages/cli/src/lib/local-tools.ts` receives consistent matches.
In `@packages/shared/src/schemas.ts`:
- Around line 60-65: The gitDiff schema’s ref field is currently free-form, so
option-like values starting with a dash can be forwarded to git and treated as
flags. Harden the validation in the schema definition for gitDiff.ref by
rejecting leading-dash inputs (or otherwise constraining the accepted ref shape)
before they reach the git call site, and keep the behavior aligned with the
existing staged/compare description.
---
Nitpick comments:
In `@packages/cli/src/providers/dialog/index.tsx`:
- Around line 33-38: The close callback in the dialog provider is performing the
onClose side effect inside the setCurrentDialog updater, which can be replayed
by React. Update the close useCallback in the dialog context so it first reads
the current dialog from state, calls currentDialog.onClose if present, and then
clears the dialog state separately; keep the logic out of the state updater and
preserve the existing close behavior.
In `@packages/shared/src/schemas.test.ts`:
- Around line 31-43: The getToolContracts tests are too permissive for Mode.PLAN
because they only assert inclusion and would miss accidental tool leaks. Tighten
the assertions in schemas.test.ts around getToolContracts so the PLAN case
verifies the exact returned contract only contains the expected git tools and
excludes write/bash tools, while the BUILD case still confirms the full expected
set; use getToolContracts and Mode as the anchor points.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8330b620-3dc9-44dc-b05a-8ef41ddc5889
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (24)
.gitignoredocs/agent-permissions.mdpackages/cli/package.jsonpackages/cli/src/components/dialogs/bash-approval-dialog.tsxpackages/cli/src/components/dialogs/index.tsxpackages/cli/src/components/messages/bot-message.tsxpackages/cli/src/hooks/use-chat.tspackages/cli/src/lib/bash-approval-ui.tspackages/cli/src/lib/bash-approval.test.tspackages/cli/src/lib/bash-approval.tspackages/cli/src/lib/dialog-action-nav.test.tspackages/cli/src/lib/dialog-action-nav.tspackages/cli/src/lib/git-tools.test.tspackages/cli/src/lib/local-tools.test.tspackages/cli/src/lib/local-tools.tspackages/cli/src/lib/ripgrep.test.tspackages/cli/src/lib/ripgrep.tspackages/cli/src/providers/dialog/index.tsxpackages/cli/src/providers/dialog/types.tspackages/cli/src/screens/session.tsxpackages/server/src/system-prompt.test.tspackages/server/src/system-prompt.tspackages/shared/src/schemas.test.tspackages/shared/src/schemas.ts
| dialog.open({ | ||
| title: "Approve dangerous command", | ||
| // Fires on Esc, backdrop click, or dialog.close() — maps to reject unless already settled. | ||
| onClose: () => settle("reject"), | ||
| children: createElement(BashApprovalDialog, { | ||
| command, | ||
| onApproveOnce: () => { | ||
| settle("approve-once"); | ||
| dialog.close(); | ||
| }, | ||
| onReject: () => { | ||
| settle("reject"); | ||
| dialog.close(); | ||
| }, | ||
| onAllowSession: () => { | ||
| settle("allow-session"); | ||
| dialog.close(); | ||
| }, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
requestBashApproval can hang if another dialog replaces it.
This promise only settles via the dialog callbacks/onClose, but DialogProvider.open() currently overwrites currentDialog directly. Any reentrant dialog.open() while this modal is pending drops the approval UI without settling the promise, so use-chat can block forever waiting for a verdict. Make open() close or settle the existing dialog before replacement.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/src/lib/bash-approval-ui.ts` around lines 31 - 50, The
requestBashApproval flow can hang because DialogProvider.open() replaces the
current dialog without settling the existing one, so the promise in
bash-approval-ui.ts never resolves when a reentrant dialog opens. Update
DialogProvider.open() to close or otherwise settle any existing dialog before
assigning the new currentDialog, and make sure the existing dialog’s onClose
path is triggered so requestBashApproval is not left pending.
|
🚅 Deployed to the MoCode-TUI-pr-13 environment in lavish-courage
|
Harden tool-call mode lookup, ripgrep argv parsing, bash blocklist coverage, gitDiff.ref validation, dialog lifecycle, and gitStatus dirty counts per PR #13 CR. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
grepimplementation to@vscode/ripgrep(native.gitignore,--no-require-gitfor non-git dirs) while keeping tool name and output shape unchanged.gitStatus/gitDifftools viasimple-gitin PLAN and BUILD modes; prefer overbash git *in system prompt.output-error).docs/agent-permissions.md, bash transcript dual-line display (command + dim description), streaming flag fix for last assistant message, and 42 regression/integration tests.Test plan
bun test packages/server/src/system-prompt.test.ts packages/cli/src/lib/bash-approval.test.ts packages/cli/src/lib/dialog-action-nav.test.ts packages/cli/src/lib/ripgrep.test.ts packages/cli/src/lib/git-tools.test.ts packages/cli/src/lib/local-tools.test.ts packages/shared/src/schemas.test.ts— 42 passrm -rf ./tmp→ TUI dialog appears; Reject → model does not ask for typed chat confirmationgitStatusworks;bashthrows guard errorgreprespects.gitignore(nonode_modulesmatches)Notes
doc/harness-phase-01-search-git-notes.mdare not in this PR (doc/is gitignored onmaster).Made with Cursor
Summary by CodeRabbit
New Features
Bug Fixes
Documentation